<?php
/**
 * @file
 * This is a filter module to generate a collapsible jquery enabled mediawiki
 * style table of contents based on <h[1-6]> tags. Transforms header tags into
 * named anchors.
 *
 * It is a complete rewrite of the original non-jquery enabled tableofcontents
 * filter module as follows:
 *   +added jquery to make ToC collapsible
 *   +preserves attributes on the header tags
 *   +checks for existing ID on headers and uses that if found (if none,
 *    creates one)
 *   +extends the minimum header level to 1
 *   +header conversion is case insensitive
 *   +made the regex match for options on the <!--tableofcontents-->
 *    marker tolerant of spaces
 *   +replaced the comment with [tag ...]
 *   +added a more explanatory error message for invalid options & concatenated
 *    it into one string to prevent duplicates from being displayed
 *   +added several divs to make ToC themable via CSS
 *   +provided basic CSS
 */

define('TABLEOFCONTENTS_REMOVE_PATTERN', '/(?:<p(?:\s[^>]*)?'.'>)?\[toc(?:\s[^]]*?)?\](?:<\/p\s*>)?/');
define('TABLEOFCONTENTS_ALLOWED_TAGS', '<em> <i> <strong> <b> <u> <del> <ins> <sub> <sup> <cite> <strike> <s> <tt> <span> <font> <abbr> <acronym> <dfn> <q> <bdo> <big> <small>');

/**
 * Implementation of hook_init()
 *
 * We load the JS/CSS files here so we can cache the filter results.
 */
function tableofcontents_init() {
  // we probably should not do that on non-node pages!
  // (how do you determine that?)
  $path = drupal_get_path('module', 'tableofcontents');
  drupal_add_js($path . '/jquery.scrollTo-min.js');
  drupal_add_js($path . '/jquery.localscroll-min.js');
  drupal_add_js($path . '/tableofcontents.js');
  drupal_add_css($path . '/tableofcontents.css');
}

/**
 * Implementation of hook_help().
 */
function tableofcontents_help($section) {
  switch ($section) {
    case 'admin/modules#description':
      return t('A module to create a table of contents based on HTML header tags. Changes headers to anchors for processing so it may be incompatible with other filters that process header tags. It does use existing IDs on the header tags if already present and only operates on header levels 1 - 6.');
  }
}

/**
 * Implementation of hook_filter_FILTER_tips().
 */
function tableofcontents_filter_FILTER_tips($filter, $format, $long = FALSE) {
  if ( $filter->name == filter_toc ) {
    $delta = 0;
  } else {
    $delta = 1;
  }
  $var = $format->format;
  $override = variable_get('tableofcontents_allow_override_' . $var, $default = TRUE);

  $op = ($delta * 4) | ($long ? 2 : 0) | ($override ? 1 : 0);

  switch ($op) { // ($delta, $long, $override)
  case 0: // (0, 0, 0)
    return t('Use [toc ...] to insert a mediawiki style collapsible table of contents.');

  case 1: // (0, 0, 1)
    return t('Use [toc list: ol; title: Table of Contents; minlevel: 2; maxlevel: 3; attachments: yes;] to insert a mediawiki style collapsible table of contents. All the arguments are optional.');

  case 2: // (0, 1, 0)
    return t('Every instance of "[toc ...]" in the input text will be replaced with a collapsible mediawiki-style table of contents.');

  case 3: // (0, 1, 1)
    return t('Every instance of "[toc ...]" in the input text will be replaced with a collapsible mediawiki-style table of contents. Accepts options for title, list style, minimum heading level, and maximum heading level, and attachments as follows: [toc list: ol; title: Table of Contents; minlevel: 2; maxlevel: 3; attachments: yes;]. All arguments are optional.');

  case 4: // (1, 0, 0)
  case 5: // (1, 0, 1)
    return t('Add an identifier to all the anchors without one.');

  case 6: // (1, 1, 0)
  case 7: // (1, 1, 1)
    return t('Add an identifier to all the anchors without one. In most cases, this filter is not necessary with the table of contents block. However, once in a while, the automatic detection may fail. In those cases, add this filter to your pages so as to make sure to get all the anchors defined as necessary for the table of contents links to work.');

  }
}

/**
 * Implementation of hook_filter_info().
 */
function tableofcontents_filter_info() {
  module_load_include('admin.inc', 'tableofcontents');
  $filters['filter_toc'] = array(
    'title' => t('Table of contents'),
    'description' => t('Inserts a table of contents in place of [toc ...] tags.'),
    'process callback' => '_tableofcontents_process',
    'prepare callback' => '_tableofcontents_prepare',
    'settings callback' => '_tableofcontents_settings',
    'cache' => TRUE,
    'tips callback' => 'tableofcontents_filter_tips',
    );
  $filters['filter_toc_id'] = array(
    'title' => t('Assign an ID to each anchors'),
    'description' => t('Add an ID to all the anchors on the page. May be necessary in case the table of contents block is used.'),
    'process callback' => '_tableofcontents_process',
    'prepare callback' => '_tableofcontents_prepare',
    'settings callback' => '_tableofcontents_settings',
    'cache' => TRUE,
    'tips callback' => 'tableofcontents_filter_tips',
  );
  return $filters;
  }

/**
 * Save the extra data the TOC adds to nodes.
 */
function _tableofcontents_save($node) {
  // new nodes will not have our parameter set, make sure we have a default
  if (!isset($node->tableofcontents_toc_automatic)) {
    $node->tableofcontents_toc_automatic = 0;
  }

  $num_updated = db_update('tableofcontents_node_toc') // Table name no longer needs {}
  ->fields(array(
    'toc_automatic' => $node->tableofcontents_toc_automatic,
  ))
  ->condition('nid', $node->nid)
  ->execute();

  if ($num_updated == 0) {
    db_insert('tableofcontents_node_toc') // Table name no longer needs {}
      ->fields(array(
        'nid' => $node->nid,
        'toc_automatic' => $node->tableofcontents_toc_automatic,
      ))
      ->execute();
  }
}

/**
 * Load the extra data for the node from the TOC table.
 */
function _tableofcontents_load(&$node) {
  $result = db_select('tableofcontents_node_toc', 'toc')
    ->fields('toc', array('toc_automatic'))
    ->condition('nid', $node->nid)
    ->execute()
    ->fetch();
  if (!empty($result) && $toc = $result->toc_automatic) {
    $node->tableofcontents_toc_automatic = $toc;
  }
  else {
    $node->tableofcontents_toc_automatic = 0;
  }
}

/**
 * Implementation of hook_node_prepare
 */
function tableofcontents_node_prepare($node) {
  if (isset($node->files) && isset($node->body)) {
    // Remove the cached version if there are attachments on this node
    $cid = $node->format . ':' . md5($node->body);
    cache_clear_all($cid, 'cache_filter');
  }
}

/**
 * Implementation of hook_node_presave
 */
function tableofcontents_node_presave($node) {
  if (isset($node->body)) {
    if (variable_get('tableofcontents_remove_teaser_' . $node->body[$node->language][0]['format'], TRUE)) {
      module_load_include('admin.inc', 'tableofcontents');
      _tableofcontents_hide_in_teaser($node);
    }
  }
}

/**
 * Implementation of hook_node_insert
 */
function tableofcontents_node_insert($node) {
  _tableofcontents_save($node);
}

/**
 * Implementation of hook_node_update
 */
function tableofcontents_node_update($node) {
  _tableofcontents_save($node);
}

/**
 * Implementation of hook_node_load
 */
function tableofcontents_node_load($nodes) {
  foreach ($nodes as $node) {
    _tableofcontents_load($node);
  }
}

/**
 * Implementation of hook_node_view
 */
function tableofcontents_node_view($node, $view_mode) {
  global $user, $theme_key, $_tableofcontents_block_processing;

  if (!$_tableofcontents_block_processing && isset($node->content['body'])) {
    $processed = FALSE;
    if (variable_get('tableofcontents_nodetype_toc_vtoc_' . $node->type, FALSE)) {
      // ugly test to make sure we don't double the TOC (i.e. if automatic is turned
      // on we would apply the TOC twice when [toc] was used and the filter includes
      // the TOC!)
      if (strpos($node->content['body'][0]['#markup'], 'class="toc"') === FALSE) {
        $node->content['body'][0]['#markup'] = str_replace('[vtoc', '[toc', $node->content['body'][0]['#markup']);
        if ($view_mode == 'teaser' && variable_get('tableofcontents_nodetype_toc_remove_from_teaser_' . $node->type, TRUE)) {
          // remove from teaser or "non-page"
          $node->content['body'][0]['#markup'] = preg_replace(TABLEOFCONTENTS_REMOVE_PATTERN, '', $node->content['body'][0]['#markup']);

        }
        else {
          // TODO: if $a3 is true, then we should process the body and save the resulting table in the teaser
          //       (in other words, make sure we get the complete table of content instead of only the teaser part!)
          module_load_include('pages.inc', 'tableofcontents');
          //$format = new stdClass();
          $format = $node->content['body']['#items'][0]['format'];
          $text = _tableofcontents_process($node->content['body'][0]['#markup'], $format);
          if ($node->content['body'][0]['#markup'] != $text) {
            // if tableofcontents_hide_table_<format> is TRUE, then this is wrong...
            if (!variable_get('tableofcontents_hide_table_' . $node->content['body']['#items'][0]['format'], FALSE)) {
              $processed = TRUE;
            }
            $node->content['body'][0]['#markup'] = $text;

          }
        }
      }
    }
    if (!$processed) {
      // is the table of contents block visible?
      $rids = array_keys($user->roles);

      $query = db_select('block', 'b')
            ->fields('b', array('bid'));
      $query->leftJoin('block_role', 'r', 'b.module = r.module AND b.delta = r.delta', array());
      $query->condition('b.delta', 'tableofcontents_block')
            ->condition('b.module', 'tableofcontents_block')
            ->condition('b.theme', array_merge(array($theme_key), $rids))
            ->condition('b.status', '1')
            ->condition(db_or()->condition('r.rid', $rids, 'IN')->isNull('r.rid'));
      $result = $query->execute()
                  ->fetchAll();

      if ($result) {
        // there is a table of contents block, but the node was node parsed...
        // do that now
        module_load_include('pages.inc', 'tableofcontents'); 
       $format = $node->content['body']['#items'][0]['format'];
       $node->content['body'][0]['#markup'] = _tableofcontents_process($node->content['body'][0]['#markup'], $format, 1);
      }
    }
  }
}

/**
 * Add a field in nodes so one can mark the node as using a TOC without
 * the need for the [toc] tag.
 */
function tableofcontents_form_alter(&$form, $form_state, $form_id) {
  if (!$form_state['submitted']) {
    if (substr($form_id, -10) == '_node_form' && isset($form['nid'])) {
      module_load_include('admin.inc', 'tableofcontents');
      _tableofcontents_node_form_alter($form);
    }
    elseif ($form_id == 'node_type_form') {
      module_load_include('admin.inc', 'tableofcontents');
      _tableofcontents_nodetype_form_alter($form);
    }
    elseif ($form_id == 'filter_admin_format_form') {
      $form['#submit'][] = '_tableofcontents_settings_submit';
    }
  }
}

/**
 * Implementation of hook_theme().
 *
 * @return
 *   Array of theme hooks this module implements.
 */
function tableofcontents_theme() {
  return array(
    'tableofcontents_toc' => array(
      'variables' => array(
        'toc' => NULL,
      ),
      'file' => 'tableofcontents.pages.inc',
    ),
    'tableofcontents_toc_text' => array(
      'arguments' => array(
        'toc' => NULL,
      ),
      'file' => 'tableofcontents.pages.inc',
    ),
    'tableofcontents_back_to_top' => array(
      'variables' => array(
        'toc' => NULL,
      ),
      'file' => 'tableofcontents.pages.inc',
    ),
    'tableofcontents_number' => array(
      'variables' => array(
        'toc' => NULL,
      ),
      'file' => 'tableofcontents.pages.inc',
    ),
    'tableofcontents_number_text' => array(
      'arguments' => array(
        'toc' => NULL,
      ),
      'file' => 'tableofcontents.pages.inc',
    ),
  );
}

// vim: ts=2 sw=2 et syntax=php
